﻿#region License
/* Copyright (C) 2009 Tim Coulter
 * 
 * This file is part of ExtremeML.
 * 
 * ExtremeML is free software: you can redistribute
 * it and/or modify it under the terms of the GNU General Public
 * License, as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 * 
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
 * the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/.
*/
#endregion

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using DocumentFormat.OpenXml.Spreadsheet;
using ExtremeML.Core;
using ExtremeML.Packaging;
using ExtremeML.Spreadsheet.Address;
using ExtremeML.Spreadsheet.Data;

namespace ExtremeML.Spreadsheet
{
	// This is a partial class that adds custom functionality to its auto-generated partner.
	public sealed partial class CellWrapper
	{
		#region Custom Properties

		/// <summary>
		/// Gets the containing <see cref="WorkbookPartWrapper"/>.
		/// </summary>
		public WorkbookPartWrapper Workbook
		{
			get { return ((SpreadsheetDocumentWrapper)Package).WorkbookPart; }
		}
	
		/// <summary>
		/// Gets the containing <see cref="WorksheetWrapper"/>.
		/// </summary>
		public WorksheetWrapper Worksheet
		{
			get { return ((WorksheetWrapper)Parent.Parent.Parent.Parent.Parent); }
		}

		/// <summary>
		/// Gets the <see cref="CellAddress"/> that manages cell addressing for the wrapped cell.
		/// </summary>
		public CellAddress CellAddress { get; private set; }

		/// <summary>
		/// Gets or sets the <see cref="SharedStringItemWrapper"/> that stores string content for the wrapped cell.
		/// </summary>
		private SharedStringItemWrapper SharedString { get; set; }

		/// <summary>
		/// Gets or sets the <see cref="HyperlinkWrapper"/> associated with the wrapped cell.
		/// </summary>
		internal HyperlinkWrapper Hyperlink { get; set; }

		#endregion
		#region Custom Methods

		/// <summary>
		/// Returns the value stored in this instance.
		/// </summary>
		/// <typeparam name="T">One of the supported data types (see remarks).</typeparam>
		/// <returns>The value of the cell converted to specified data type or <c>null</c> if the cell data cannot be parsed into the specified type.</returns>
		/// <remarks>
		/// Any of the following supported data types may be specified in the type argument and, where
		/// appropriate, will result in a type-specific translation of the cell data stored by Excel.
		/// <list type="bullet">
		/// <item><description><see cref="string"/></description></item>
		/// <item><description><see cref="Uri"/></description></item>
		/// <item><description><see cref="HyperlinkUri"/></description></item>
		/// <item><description>Nullable <see cref="short"/></description></item>
		/// <item><description>Nullable <see cref="int"/></description></item>
		/// <item><description>Nullable <see cref="long"/></description></item>
		/// <item><description>Nullable <see cref="decimal"/></description></item>
		/// <item><description>Nullable <see cref="float"/></description></item>
		/// <item><description>Nullable <see cref="double"/></description></item>
		/// <item><description>Nullable <see cref="DateTime"/></description></item>
		/// <item><description>Nullable <see cref="TimeSpan"/></description></item>
		/// <item><description>Nullable <see cref="bool"/></description></item>
		/// </list>
		/// </remarks>
		public T GetValue<T>()
		{
			try
			{
				if (typeof(T).Equals(typeof(string)))
				{
					return (T)Convert.ChangeType(GetRawValue(), typeof(T));
				}
				else if (typeof(T).Equals(typeof(HyperlinkUri)))
				{
					if (Hyperlink == null)
					{
						return default(T);
					}
					else
					{
						return (T)Convert.ChangeType(new HyperlinkUri(Hyperlink.Relationship.Uri.AbsoluteUri, GetRawValue()), typeof(T));
					}
				}
				else if (typeof(T).Equals(typeof(Uri)))
				{
					return (T)Convert.ChangeType(new Uri(GetRawValue()), typeof(T));
				}
				else if (typeof(T).Equals(typeof(short?))
					|| typeof(T).Equals(typeof(int?))
					|| typeof(T).Equals(typeof(long?))
					|| typeof(T).Equals(typeof(decimal?))
					|| typeof(T).Equals(typeof(float?))
					|| typeof(T).Equals(typeof(double?)))
				{
					return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(GetRawValue());
				}
				else if (typeof(T).Equals(typeof(bool?)))
				{
					return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(GetFormula());
				}
				else if (typeof(T).Equals(typeof(DateTime?)))
				{
					DateTime? result = DecodeCellDateValue(double.Parse(GetRawValue()));
					return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(result);
				}
				else if (typeof(T).Equals(typeof(TimeSpan?)))
				{
					TimeSpan? result = DecodeCellTimeValue(double.Parse(GetRawValue()));
					return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(result);
				}
				else
				{
					throw new NotSupportedException(string.Format("Date type '{0}' is not supported in cell values.", typeof(T).Name));
				}
			}
			catch
			{
				return default(T);
			}
		}

		/// <summary>
		/// Sets the content of this instance to the passed value.
		/// </summary>
		/// <param name="value">The value to be assigned to the cell.</param>
		/// <remarks>
		/// Any of the following supported data types may be passed and, where appropriate,
		/// will result in a type-specific translation of the passed value into a format
		/// that is directly compatible with the storage formats used by Excel.
		/// <list type="bullet">
		/// <item><description><see cref="string"/></description></item>
		/// <item><description><see cref="Uri"/></description></item>
		/// <item><description><see cref="HyperlinkUri"/></description></item>
		/// <item><description>Nullable <see cref="short"/></description></item>
		/// <item><description>Nullable <see cref="int"/></description></item>
		/// <item><description>Nullable <see cref="long"/></description></item>
		/// <item><description>Nullable <see cref="decimal"/></description></item>
		/// <item><description>Nullable <see cref="float"/></description></item>
		/// <item><description>Nullable <see cref="double"/></description></item>
		/// <item><description>Nullable <see cref="DateTime"/></description></item>
		/// <item><description>Nullable <see cref="TimeSpan"/></description></item>
		/// <item><description>Nullable <see cref="bool"/></description></item>
		/// </list>
		/// </remarks>
		public void SetValue(
			object value)
		{
			if (DataType.HasValue && DataType.Value == CellValues.InlineString)
			{
				// Since this library uses shared strings, any existing inline
				// strings can be eliminated when their values are updated.
				InlineString.Initialize();
			}

			if (SharedString != null)
			{
				// Any existing reference to a shared string can be released.
				SharedString.DecrementReferenceCount();
			}

			if (Hyperlink != null)
			{
				// Any existing associated hyperlink can be deleted.
				Hyperlink.Remove();
			}

			if (value == null)
			{
				CellValue.Initialize();
			}
			else if (value is string)
			{
				DataType = CellValues.SharedString;
				SharedString = Workbook.SharedStringTablePart.SharedStringTable.GetStringItem((string)value);
			}
			else if (value is HyperlinkUri)
			{
				DataType = CellValues.SharedString;
				SharedString = Workbook.SharedStringTablePart.SharedStringTable.GetStringItem((value as HyperlinkUri).Text);
				Worksheet.Hyperlinks.Hyperlinks.Add(value as HyperlinkUri, CellAddress);
			}
			else if (value is Uri)
			{
				DataType = CellValues.SharedString;
				SharedString = Workbook.SharedStringTablePart.SharedStringTable.GetStringItem((value as Uri).AbsoluteUri);
				Worksheet.Hyperlinks.Hyperlinks.Add(value as Uri, CellAddress);
			}
			else if (value is short
				|| value is int
				|| value is long)
			{
                DataType = CellValues.Number;
                CellValue.Value = value.ToString();
            }
            else if (value is decimal)
			{
				DataType = CellValues.Number;
			    CellValue.Value = ((decimal) value).ToString(CultureInfo.InvariantCulture);
            }
			else if (value is float)
			{
				DataType = CellValues.Number;
				CellValue.Value = ConvertToLongString(Convert.ToDouble((float)value));
			}
			else if (value is double)
			{
				DataType = CellValues.Number;
				CellValue.Value = ConvertToLongString((double)value);
			}
			else if (value is DateTime)
			{
				DataType = CellValues.Number;
				CellValue.Value = ConvertToLongString(EncodeCellDateValue((DateTime)value));
			}
			else if (value is TimeSpan)
			{
				DataType = CellValues.Number;
				CellValue.Value = ConvertToLongString(EncodeCellTimeValue((TimeSpan)value));
			}
			else if (value is bool)
			{
				DataType = CellValues.Boolean;
				SetFormula(value.ToString().ToUpper());
				CellValue.Value = ((bool)value ? 1 : 0).ToString();
			}
			else
			{
				// Unsupported data types are treated as null.
				CellValue.Initialize();
			}
		}

		/// <summary>
		/// Sets the formula of this instance to the passed value.
		/// </summary>
		/// <param name="value">The formula to be assigned to the cell.</param>
		public void SetFormula(
			string value)
		{
			SetValue(null);
			CellFormula.Value = value;
		}

		/// <summary>
		/// Returns the formula assigned to this instance.
		/// </summary>
		/// <returns>The formula assigned to the cell.</returns>
		public string GetFormula()
		{
			return CellFormula.Value;
		}

		#endregion
		#region Overrides

		/// <summary>
		/// Implements custom initialization functionality.
		/// </summary>
		protected override void OnCustomInitializing()
		{
			CellAddress = new CellAddress();
		}

		/// <summary>
		/// Implements custom population functionality.
		/// </summary>
		protected override void OnCustomDecomposing()
		{
			CellAddress = AddressParser.ParseCellAddress(CellReference);
		}

		/// <summary>
		/// Establishes relationships with other package content objects.
		/// </summary>
		protected override void OnCustomActivating()
		{
			RegisterAddressDependencies();

			if (DataType.HasValue && DataType.Value == CellValues.SharedString)
			{
				SharedString = Workbook.SharedStringTablePart.SharedStringTable.GetStringItem(int.Parse(CellValue.Value));
			}
		}

		/// <summary>
		/// Implements custom persistence functionality.
		/// </summary>
		protected override void OnCustomRecomposing(
			Cell wrapped)
		{
			CellReference = CellAddress.Reference;

			// Formula cells must be null to ensure re-calculation when the package is re-opnened.
			if (CellFormula.Value != null)
			{
				SetValue(null);
			}

			if (DataType.HasValue && DataType.Value == CellValues.SharedString)
			{
				CellValue.Value = SharedString.GetIndex().ToString();
			}
		}

		/// <summary>
		/// Implements custom deletion functionality.
		/// </summary>
		protected override void OnCustomRemoved()
		{
			ReleaseAddressDependencies();

			if (SharedString != null)
			{
				SharedString.DecrementReferenceCount();
			}

			if (Hyperlink != null)
			{
				Hyperlink.Remove();
			}
		}

		/// <summary>
		/// Returns the set of <see cref="IWrapper"/> instances defined as custom properties.
		/// </summary>
		protected override IEnumerable<IWrapper> GetCustomWrapperMembers()
		{
			yield return CellAddress;
		}

		#endregion
		#region Implementation Methods

		/// <summary>
		/// Registers addressable contained objects for notification of address changes.
		/// </summary>
		private void RegisterAddressDependencies()
		{
			Worksheet.WorksheetPart.AddressDependencies.Add(CellAddress);

			CellAddress.CellDeleted += HandleCellDeletion;
		}

		/// <summary>
		/// Releases previously registered contained objects from notification of address changes.
		/// </summary>
		private void ReleaseAddressDependencies()
		{
			if (Worksheet.WorksheetPart.AddressDependencies.Contains(CellAddress))
			{
				Worksheet.WorksheetPart.AddressDependencies.Remove(CellAddress);
			}

			CellAddress.CellDeleted -= HandleCellDeletion;
		}

		/// <summary>
		/// Handles the cell deletion event of <see cref="CellAddress"/>.
		/// </summary>
		private void HandleCellDeletion(
			object sender,
			EventArgs e)
		{
			Remove();
		}

		/// <summary>
		/// Returns the stored cell value prior to any type conversion.
		/// </summary>
		private string GetRawValue()
		{
			if (DataType.HasValue)
			{
				switch (DataType.Value)
				{
					case CellValues.InlineString: return InlineString.Value;
					case CellValues.SharedString: return SharedString.Value;
					default: return CellValue.Value;
				}
			}
			else
			{
				return CellValue.Value;
			}
		}

		/// <summary>
		/// Returns whether the passed object value is a numeric type.
		/// </summary>
		private static bool IsNumeric(
			object value)
		{
			return
				value is int ||
				value is Int16 ||
				value is Int32 ||
				value is Int64 ||
				value is UInt16 ||
				value is UInt32 ||
				value is UInt64 ||
				value is decimal ||
				value is Single ||
				value is double;
		}

		/// <summary>
		/// Converts the passed <see cref="double"/> to its fully expanded string representation.
		/// </summary>
		/// <remarks>
		/// Thanks to "psasik" for sharing this code: http://stackoverflow.com/questions/1546113/double-to-string-conversion-without-scientific-notation
		/// </remarks>
		private static string ConvertToLongString(
			double value)
		{
            var raw = value.ToString(CultureInfo.InvariantCulture).ToUpper();

			// If string representation was collapsed from scientific notation, just return it.
			if (!raw.Contains("E")) return raw;

            var separator = CultureInfo.InvariantCulture.NumberFormat.NumberDecimalSeparator.ToCharArray()[0];

// Modified 2010-01-20 due to bugs with TimeSpan in Swedish culture
//var separator = Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator.ToCharArray()[0];
			var exponentParts = raw.Split('E');
			var decimalParts = exponentParts[0].Split(separator);

			// Fix missing decimal point.
			if (decimalParts.Length == 1)
			{
				decimalParts = new string[] { exponentParts[0], "0" };
			}

			var exponentValue = int.Parse(exponentParts[1]);
			var sign = decimalParts[0].StartsWith("-") ? "-" : "";
			decimalParts[0] = decimalParts[0].TrimStart('-');
			var declaredDigits = (decimalParts[0] + decimalParts[1]);

			if (exponentValue > 0)
			{
				return sign + declaredDigits + new string('0', Math.Abs(exponentValue - decimalParts[1].Length));
			}
			else // Negative exponent
			{
				return (sign + "0" + separator + new string('0', Math.Abs(exponentValue + decimalParts[0].Length)) + declaredDigits).TrimEnd('0');
			}
		}

		/// <summary>
		/// Converts the passed <see cref="TimeSpan"/> into an double cell value.
		/// </summary>
		private static double EncodeCellTimeValue(
			TimeSpan time)
		{
			return time.TotalSeconds / 86400d;
		}

		/// <summary>
		/// Converts the passed double cell value into a <see cref="TimeSpan"/>.
		/// </summary>
		private static TimeSpan DecodeCellTimeValue(
			double value)
		{
			return new TimeSpan(0, 0, (int)Math.Round(86400 * value, 0));
		}

		/// <summary>
		/// Converts the passed <see cref="DateTime"/> into an integer cell value.
		/// </summary>
		private static double EncodeCellDateValue(
			DateTime date)
		{
			var parseDate = new DateTime(1899, 12, 30);
			return (date - parseDate).TotalSeconds / 86400d;
		}

		/// <summary>
		/// Converts the passed double cell value into a <see cref="DateTime"/>.
		/// </summary>
		private static DateTime DecodeCellDateValue(
			double value)
		{
            var parseDate = new DateTime(1899, 12, 30);
            return parseDate.AddDays(value);
        }

		#endregion
		#region Static Methods


		#endregion
	}
}
